#!/usr/bin/env python
#-*- coding:utf-8 -*-

"""
    El modulo **motor** proporciona clases de utilidad, para la construccion
    de un motor de inferencia, en conjunto con una manera de definir reglas y
    hechos que las disparen, como asi tambien poder consultar el estado del
    grafo subyacente de la red de inferencia construida.

    Operatoria del motor
    --------------------
        #. Definir atributos posibles: nombres (todos son strings).
        #. Definir reglas: a partir de un string.
        #. Construir red de ``hechos->reglas``
        #. Esperar el ingreso nuevos hechos.
        #. Procesar hasta terminar.


    Nodos
    -----
        La red de inferencia contiene los siguientes tipos de nodos.

        * Nodo de atributos:
            *nombre + setDeHechos*.
        * Nodo de operadores vs atributo:
            *nombre + nodo_atr1 + operador + nodo_atr2*
        * Nodo de regla:
            *nombre + setDeNodosOperadores + consecuencia.*
"""

#import sys
import mixins

# INTERPRETE DE ECUACIONES.
from ecuacion import Ecuacion

# Delegados para las comparaciones y aplicacion de operadores.
import evaluador

class Hecho (mixins.Nombre):
    """
        El motor de inferencia es alimentado con hechos; es decir un conjunto de
        pares atributo=valor, que indican una situacion particular.
        El conjunto de hechos es completamente editable.
        Puede ser construido a partir de un string.
        Se puede saber si dos hechos tiene los mismos pares atributo=valor.

    """
    def __init__(self, nombre):
        mixins.Nombre.__init__(self, nombre)
        self._atributos = dict()

    def get_atributo(self, atributo):
        """
            Obtiene el valor de un atributo por su nombre.
        """
        return self._atributos[atributo]

    def set_atributo(self, atributo, valor):
        """
            Modifica el valor de un atributo.            
        """
        self._atributos[atributo] = valor

    def get_atributos(self):
        """
            Obtiene un diccionario con los pares atributo=valor del hecho.
        """
        return self._atributos

    def list_atributos(self):
        """
            Obtiene el nombre de los atributos del hecho.
        """
        return list( self._atributos.keys() )

    def __str__(self):
        nombre = self.get_nombre()
        if nombre in self._atributos:
            valor = self.get_atributo(nombre)
            if valor == nombre:
                return str(valor)
            else:
                return str(nombre)+'='+str(valor)
        else:
            aux = list()
            for atr, val in self._atributos.items():
                if str(atr) != str(val):
                    aux.append( str(atr) + '=' + str(val) )
                else:
                    aux.append(atr)

            aux = '('+ ', '.join(aux) +')'
            if len(self.get_nombre()) > 0:
                return self.get_nombre()+': '+aux
            return aux

    @classmethod
    def from_string(cls, string):
        """
            Metodo constructor a partir de un string.
            Formato esperado: NOMBRE ATR1=VAL1 ATR2=VAL2 ATR3=VAL3
        """
        partes = string.split()

        nuevo = None
        if len(partes) > 0:
            if len(partes) > 1:
                nuevo = cls(partes[0])
                del partes[0]
            else:
                nuevo = cls(partes[0])

            for parte in partes:
                atr_val = parte.split('=', 1)
                if len(atr_val) == 2:
                    nuevo.set_atributo(atr_val[0].strip(), atr_val[1].strip())
        return nuevo

    def equivalente(self, other):
        """

            Dos hechos se consideran equivalentes si:
                * Tienen los mismos atributos.
                * Todos sus pares atributo=valor son iguales.

        """
        if self == other:
            return True
    
        remotos = other.list_atributos()
        locales = self.list_atributos()

        exito = len(remotos) == len(locales)
    
        if exito:
            for atr in remotos:
                local = None
                try:
                    local = self.get_atributo(atr)
                    remoto = other.get_atributo(atr)
                    exito = local == remoto
                except KeyError:
                    exito = False

                if not exito:
                    break
        return exito


class HechoAglomerado(Hecho, mixins.Output):
    """
        Permite generar hechos que son la combinacion de otros.
        En escencia genera un arbol de hechos.
    """
    def __init__(self, nombre, resultado = object()):
        Hecho.__init__(self, nombre)
        mixins.Output.__init__(self)
        self._resultado = resultado

    def set_resultado(self, resultado):
        self._resultado = resultado
    
    def get_resultado(self):
        return self._resultado
    def add_hecho(self, hecho):
        exito = True
        if not hecho in self.get_outputs():
            # Un hecho puede mezclarse con otro, solo si no tiene atributos
            #  en colision con otro, es decir que si ambos tienen los mismos
            #  atributos, estos valen igual.
            hay_colision = False
            # Para c/ hecho ya aglomerado, compruebo que no existan colisiones.
            for anteriores in self.get_outputs():
                # Obtengo la list_a de los atributos que estan en ambos.
                interseccion = set(self.list_atributos())
                interseccion.intersection_update( anteriores.list_atributos() )
    
                if not interseccion is None:
                    for atr in interseccion:
                        valor = anteriores.get_atributo(atr)
                        hay_colision = self.get_atributo(atr) != valor
                        if hay_colision:
                            break
                
                if hay_colision:
                    break
            if not hay_colision:
                self.add_output(hecho)
            exito = not hay_colision

        return exito

    def set_atributo(self, atributo, valor):
        pass

    def get_atributo(self, atributo):
        valor = None
        for vecino in self.get_outputs():
            try:
                valor = vecino.get_atributo(atributo)
            except KeyError:
                valor = None
                            
            if not valor is None:
                break
        return valor

    def list_atributos(self):
        atributos = set()
        for vecino in self.get_outputs():
            for atr in vecino.list_atributos():
                atributos.add(atr)
        return atributos

    # Crea un nuevo hecho, que tiene todos los atributos de la aglomeracion.
    def merge(self):
        nuevo = Hecho( '' )
        
        for atr in self.list_atributos():
            nuevo.set_atributo(atr, self.get_atributo(atr))

        return nuevo

    def contains(self, hecho):
        contenido = False
        for vecino in self.get_outputs():
            # Si tiene el hecho no continuo.
            if vecino == hecho:
                contenido = True
                break
            # Si tiene otros hechos dentro.
            if isinstance(vecino, HechoAglomerado):
                if vecino.contains(hecho):
                    contenido = True
                    break
        return contenido

    def __str__(self):
        aux = '{ '
        for vecino in self.get_outputs():
            if isinstance(vecino, HechoAglomerado):
                aux += str(vecino)+', '
            else:
                aux += str(vecino)+', '
        aux += '}'
        return aux


class Atributo (mixins.Nombre, mixins.Output, mixins.Entrenamiento):
    """
        Un atributo es un nodo de ingreso a la red, los hechos se propagan a
        partir de los nodos que representan los atributos para los que tienen
        valor asigando.
    """
    def __init__(self, nombre):
        mixins.Nombre.__init__(self, nombre)
        mixins.Output.__init__(self)
        mixins.Entrenamiento.__init__(self, True)

        self.hechos = set()

    def foward_cast(self, origin, hecho):
        if self.get_nombre() in hecho.get_atributos():
            if self.get_entrenamiento():
                # Si estoy en modo entrenamiento se que guardo los hechos
                #  anteriores (asi puedo evitar propagar nuevamente hechos).
                if not hecho in self.hechos:
                    mixins.Output.foward_cast(self, origin, hecho)
                    self.hechos.add(hecho)
            else:
                mixins.Output.foward_cast(self, origin, hecho)

    def __str__(self):
        nombres = [ mixins.Nombre.get_nombre(x) for x in self.get_outputs() ]
        return self.get_nombre()+' out: ' + ', '.join(nombres)


class Constante(mixins.Nombre):
    def __init__(self, nombre, valor):
        mixins.Nombre.__init__(self, nombre)
        self._valor = valor

    def set_valor(self, valor):
        self._valor = valor

    def get_valor(self):
        return self._valor

class _LinkHandler (object):
    """
        Atributos utilizados por OperadorBinario para sus enlaces entrantes.
    """
    def __init__(self, nodo):
        self.nodo = nodo
        self.hechos = set()

    def get_nombre(self):
        return self.nodo.get_nombre()

    def get_valor(self, hecho):
        valor = None

        if isinstance(self.nodo, Constante):
            valor = self.nodo.get_valor()

        elif isinstance(self.nodo, OperadorBinario):
            if isinstance(hecho, HechoAglomerado):
                valor = hecho.get_resultado()
            else:
                valor = hecho.get_atributo(self.get_nombre())

        elif isinstance(self.nodo, Atributo):
            valor = hecho.get_atributo(self.get_nombre())

        else:
            valor = hecho.get_atributo(self.get_nombre())

        return valor
    
class OperadorBinario (mixins.Nombre, mixins.Output, mixins.Entrenamiento):
    """
        Se encarga de operar con los atributos de hechos (compatibles) a fin de
            propagar resultados intermedios de reglas.
        Se espera que operacion sea un objeto con el metodo evaluar(self,A,B)
            y devuelve verdadero o falso.
    """
    def __init__(self, nombre, nodo_a, operador, nodo_b):
        mixins.Nombre.__init__(self, nombre)
        mixins.Output.__init__(self)
        mixins.Entrenamiento.__init__(self, False)

        self.in_a = _LinkHandler(nodo_a)
        self.in_b = _LinkHandler(nodo_b)

        self.operador = evaluador.factory.create(operador)

        # conjunto de tuplas de la forma (hecho (de A), hecho (de B), valor)
        self.resultados = set()

    @classmethod
    def _resultado_valido(cls, hecho):
        return not hecho.get_resultado is None

    def _armar_resultado(self, hecho_a, hecho_b):
        resultado = None
        valor_a = None
        valor_b = None

        try:
            valor_a = self.in_a.get_valor(hecho_a)
            valor_b = self.in_b.get_valor(hecho_b)

            nombre_op = self.operador.get_nombre()
            resultado = HechoAglomerado( self.get_nombre() )

            compatibles = True
#            if not self.in_a.nodo is None:
            compatibles&= resultado.add_hecho(hecho_a)
#           if not self.in_b.nodo is None:
            compatibles&= resultado.add_hecho(hecho_b)
            if not compatibles:
                resultado = None
        except KeyError as e:
            compatibles = False
            resultado = None

        return (resultado, valor_a, valor_b)

    def _operar(self, hecho_a, hecho_b):
        (resultado, valor_a, valor_b) = self._armar_resultado(hecho_a, hecho_b)
        if not resultado is None:
            valor = self.operador( valor_a, valor_b )
            resultado.set_resultado( valor )
            # Si el resultado es satisfactorio se propaga.
            if self._resultado_valido( resultado ):
                mixins.Output.foward_cast(self, self, resultado )

            if self.get_entrenamiento():
                self.resultados.add( (hecho_a, hecho_b, resultado) )
        else:
            resultado = None

        return resultado

    def foward_cast(self, origin, message):
        """
            Un operador no propaga el mismo hecho, sino que realiza realiza
                la operacion y propaga un hecho aglomerado con el resultado.
        """

        if isinstance(message, Hecho):
            # Si es un hecho de A.
            if origin == self.in_a:
                # Si es nuevo
                if not message in self.in_a.hechos:
                    # Operar con cada hecho que llego de B.
                    for anterior in self.in_b.hechos:
                        self._operar( message, anterior)
                if self.get_entrenamiento():
                    self.in_a.hechos.add(message)
            # Si es un hecho de B.
            if origin == self.in_b.nodo:
                # Si es nuevo
                if not message in self.in_b.hechos:
                    # Operar con cada hecho que llego de A.
                    for anterior in self.in_a.hechos:
                        self._operar( anterior, message)
                    if self.get_entrenamiento():
                        self.in_b.hechos.add(message)

            if not self.get_entrenamiento():
                self._operar(message, message)                  

class Regla (mixins.Nombre, mixins.Output):#, mixins.Entrenamiento):
    def __init__(self, nombre):
        mixins.Nombre.__init__(self, nombre)
        mixins.Output.__init__(self)
#        mixins.Entrenamiento.__init__(self, True)
        # TODO soportar operadores de aridad diferente a 2.
        aux = { op:2 for op in evaluador.factory.get_definidos() }
        self.ecuacion = Ecuacion(aux)
        self.hechos = set()

    def get_hechos(self):
        return self.hechos

    def foward_cast(self, origin, hecho):
        if hecho.get_resultado():
            agregar = True
            if len(self.hechos) > 0:
                for anterior in self.hechos:
                    if anterior.equivalente(hecho):
                        agregar = False
                        break
            if agregar:
                self.consecuencia(hecho)

    @classmethod
    def from_string(cls, string):
        partes = string.split(':')
        nombre = partes[0]
        nueva = Regla(nombre)
        nueva.ecuacion.from_infija(partes[-1])

        return nueva

    def consecuencia(self, hecho):
        # Consecuencia basica; agregar el hecho a los activadores de la regla.
        self.hechos.add(hecho)

    def __str__(self):
        nombre = self.get_nombre()
        ecuacion = self.ecuacion.to_infija()
        return nombre+": "+ecuacion

class RedDeInferencia(mixins.Nombre, mixins.Entrenamiento):
    def __init__(self, nombre):
        mixins.Nombre.__init__(self, nombre)
        mixins.Entrenamiento.__init__(self, True)

        # Estos conjuntos conforman la base de datos de hechos y reglas.
        # Conjuntos de nodos de la red.
        self.atributos = dict()
        self.constantes = dict()
        self.operadores = dict()
        self.reglas = dict()

    # Agrega un atributo al diccionario para que pueda ser usado por las reglas
    #  (si no existe ya).
    def definir_atributo(self, nombre):
        exito = False
        # Un atributo no puede definirse si ya existe,
        #  o hay una constante con ese nombre.
        if not nombre in self.atributos:
            if not nombre in self.constantes:
                self.atributos[nombre] = Atributo(nombre)
                exito = True
        return exito
    
    # Agrega una constante si no existe.
    def definir_constante(self, nombre, valor):
        exito = False
        if not nombre in self.atributos:
            if not nombre in self.constantes:
                self.constantes[nombre] = Constante(nombre,valor) #valor
                exito = True
        return exito

    def set_entrenamiento(self, valor):
        mixins.Entrenamiento.set_entrenamiento(self, valor)
        for operador in self.operadores.values():
            operador.set_entrenamiento(valor)
#        for regla in self.reglas.values():
#            regla.set_entrenamiento(valor)
        for atributo in self.atributos.values():
            atributo.set_entrenamiento(valor)

    def propagar(self, hecho):
        for atr in self.atributos.values():
            atr.foward_cast(None, hecho)
        
    def definir_regla(self, string):
        if not self.get_entrenamiento():
            return False

        nueva = Regla.from_string(string)

        pila_de_nodos = list()
        atributos_de_regla = set()

        for parte in nueva.ecuacion.get_componentes():
            # Si es un operador en la pila de nodos deben existir
            #  nodoR.ecuacion.operadores[c].aridad nodos.
            aridad = nueva.ecuacion.aridad_de(parte)
            if aridad > -1:
                # Agrego los atributos como nodos adyacentes al nodo operacion.
                # TODO operadores no binarios?
                if (aridad == 2):
                    nodo_b = pila_de_nodos.pop()
                    nodo_a = pila_de_nodos.pop()

                    nombre = nodo_a.get_nombre() +' '+ parte +' '+ nodo_b.get_nombre()
                    if nombre[0] == '(':
                        nombre = '[' + nombre + ']'
                    else:
                        nombre = '(' + nombre + ')'

                    try:
                        # Trato de obtener el operador por su nombre.
                        nodo_op = self.operadores[nombre]
                    except KeyError:
                        # Si no esta en self.operadores;
                        #  es nuevo y debo crearlo.
                        nodo_op = OperadorBinario(nombre, nodo_a, parte, nodo_b)
                        
                        nodo_op.set_entrenamiento(self.get_entrenamiento())
                        self.operadores[nombre] = nodo_op

                    if isinstance(nodo_a, mixins.Output):
                        nodo_a.add_output(nodo_op)
                    if isinstance(nodo_b, mixins.Output):
                        nodo_b.add_output(nodo_op)

                # Apilo el nodo_op
                pila_de_nodos.append(nodo_op)
            else:
                # Si no es una operacion debe ser un atributo o una constante.
                if parte in self.atributos:
                    nodo_atr = self.atributos[parte]
                    pila_de_nodos.append(nodo_atr)
                    atributos_de_regla.add(parte)
                    # Para propagar los hechos anteriores.
                else:
                    try:
                        nodo_cte = self.constantes[parte]
                    except KeyError:
                        nodo_cte = Constante(parte,parte)
                        self.constantes[parte] = nodo_cte
                        
                    pila_de_nodos.append(nodo_cte)
        nodo_op = pila_de_nodos.pop()
        nodo_op.add_output(nueva)
#       nodoR.addInput(nodo_op)
        self.reglas[nueva.get_nombre()] = nueva
    
        # Obtengo los hechos ya procesados de los atributos de la regla.
        hechos_viejos = set()
        for atr in atributos_de_regla:
            for viejo in self.atributos[atr].hechos:
                if viejo not in hechos_viejos:
                    # Propago nuevamente los hechos (solo una vez cada uno).
                    hechos_viejos.add(viejo)
                    self.propagar(viejo)

        return True
    
if __name__ == '__main__':
    pass
